package agents;

import gui.MachineGui;
import jade.core.AID;
import jade.core.behaviours.CyclicBehaviour;
import jade.core.behaviours.TickerBehaviour;
import jade.core.behaviours.WakerBehaviour;
import jade.domain.DFService;
import jade.domain.FIPAException;
import jade.domain.FIPANames;
import jade.domain.FIPAAgentManagement.DFAgentDescription;
import jade.domain.FIPAAgentManagement.FailureException;
import jade.domain.FIPAAgentManagement.NotUnderstoodException;
import jade.domain.FIPAAgentManagement.RefuseException;
import jade.domain.FIPAAgentManagement.ServiceDescription;
import jade.gui.GuiAgent;
import jade.gui.GuiEvent;
import jade.lang.acl.ACLMessage;
import jade.lang.acl.MessageTemplate;
import jade.proto.ContractNetInitiator;
import jade.proto.ContractNetResponder;

import java.io.IOException;
import java.util.Date;
import java.util.Enumeration;
import java.util.Vector;

import model.Position;
import model.Product;
import model.Stage;
import model.Step;
import agentInterface.MachineDetails;

public class MachineAgent extends GuiAgent {
	private static final long serialVersionUID = 1L;

	private AID factory;
	private MachineGui myGui;

	private Stage type;
	private Position position;
	private Product currentProduct;
	private int operationDuration = 1000;
	private Vector<AID> agvs = new Vector<AID>();
	private Vector<AID> macs = new Vector<AID>();
	private AID nextMachine;
	private Position nextMachinePosition;
	private Boolean processingFinished = false;
	private Boolean waiting = false;

	protected void setup() {
		System.out.println(getLocalName() + ": Hello World!");

		registerOnDF();

		getFactory();

		getAgvList();

		getMacList();

		initGui();

		AnswerOtherMachines();
		
		answerAgvsRequests();

		negotiateWithOtherMachines();
		
		processingLoop();
		
		receiveProcessingOrders();
	}

	private void receiveProcessingOrders() {
			addBehaviour(new CyclicBehaviour(this) {
				private static final long serialVersionUID = 1L;

				@Override
				public void action() {
					ACLMessage msg = receive(MessageTemplate.MatchPerformative(ACLMessage.CONFIRM));
					if (msg != null) {
						waiting = true;
						updateStatusOnFactory();
					}
				}
			});
			
		
		
	}

	private void processingLoop() {
		addBehaviour(new TickerBehaviour(this, 1000) {
			private static final long serialVersionUID = 1L;

			@Override
			protected void onTick() {
				//processingFinished && isnt already waiting for an agv, i.e. doesnt have an agv schedualed
				if(currentProduct != null && processingFinished && !waiting){
					System.out.println(getLocalName() + ": I have a " + currentProduct.getName()  + " that needs to be transfered");
					negotiateWithOtherMachines();
				}
			}
		});
		
	}

	public void negotiateWithAllAgvs() {
		System.out.print(getLocalName() + ": I want an agv to deliver a ");
		System.out.print(currentProduct.getName() + " to ");
		System.out.print(nextMachine.getLocalName() + " for ");
		System.out.println(type.nextStage().processToString() + " (sending CFPs)");
		
		ACLMessage msg = new ACLMessage(ACLMessage.CFP);
		for (int i = 0; i < agvs.size(); ++i) {
			msg.addReceiver(agvs.get(i));
		}
		msg.setProtocol(FIPANames.InteractionProtocol.FIPA_CONTRACT_NET);
		// We want to receive a reply in 10 secs
		msg.setReplyByDate(new Date(System.currentTimeMillis() + 10000));
		
		//Before constructing the step this machine has to update the current nextMachine, because it might be processing i.e. occupied
		//if we cant get a next machine, do it again untill it is possible, dont comunicate with the AGVs before that
		
		Step s = new Step(getAID(), nextMachine, position, nextMachinePosition,
				currentProduct);
		try {
			msg.setContentObject(s);
		} catch (IOException e1) {
			e1.printStackTrace();
		}

		addBehaviour(new ContractNetInitiator(this, msg) {
			private static final long serialVersionUID = 1L;

			protected void handlePropose(ACLMessage propose, Vector v) {
//				System.out.println("Agent " + propose.getSender().getName()
//						+ " proposed " + propose.getContent());
			}

			protected void handleRefuse(ACLMessage refuse) {
//				System.out.println("Agent " + refuse.getSender().getName()
//						+ " refused");
			}

			protected void handleFailure(ACLMessage failure) {
				if (failure.getSender().equals(myAgent.getAMS())) {
					// FAILURE notification from the JADE runtime: the receiver
					// does not exist
					System.out.println("Responder does not exist while negotiating with agvs");
				} else {
					System.out.println("Agent " + failure.getSender().getName()
							+ " failed");
				}
				// Immediate failure --> we will not receive a response from
				// this agent
			}

			protected void handleAllResponses(Vector responses,
					Vector acceptances) {
				// if (responses.size() < nResponders) {
				// Some responder didn't reply within the specified timeout
				// System.out.println("Timeout expired: missing "+(nResponders -
				// responses.size())+" responses");
				// }
				// Evaluate proposals.
				float bestProposal = 1000000f;
				AID bestProposer = null;
				ACLMessage accept = null;
				Enumeration e = responses.elements();
				while (e.hasMoreElements()) {
					ACLMessage msg = (ACLMessage) e.nextElement();
					if (msg.getPerformative() == ACLMessage.PROPOSE) {
						ACLMessage reply = msg.createReply();
						reply.setPerformative(ACLMessage.REJECT_PROPOSAL);
						acceptances.addElement(reply);
						float proposal = Float.parseFloat(msg.getContent());
						// finding the lowest cost proposal/plan
						if (proposal > 0 && proposal < bestProposal) {
							bestProposal = proposal;
							bestProposer = msg.getSender();
							accept = reply;
						}
					}
				}
				// Accept the proposal of the best proposer
				if (accept != null) {
					System.out.println(getLocalName() + ": I choose " +  bestProposer.getLocalName() + " to transfer " + currentProduct.getName());
					
					accept.setPerformative(ACLMessage.ACCEPT_PROPOSAL);
					waiting= true;
					updateStatusOnFactory();
				}
			}

			protected void handleInform(ACLMessage inform) {
//				System.out.println("Agent " + inform.getSender().getName()
//						+ " successfully performed the requested action");
				
			}
		});
	}

	private void negotiateWithOtherMachines() {
		System.out.println(getLocalName() + ": I want a " + type.nextStage().toString() + " machine (sending CFPs)");
		ACLMessage msg = new ACLMessage(ACLMessage.CFP);
		for (int i = 0; i < macs.size(); ++i) {
			msg.addReceiver(macs.get(i));
		}
		msg.setProtocol(FIPANames.InteractionProtocol.FIPA_CONTRACT_NET);
		// We want to receive a reply in 10 secs
		msg.setReplyByDate(new Date(System.currentTimeMillis() + 10000));

		msg.setContent(type.nextStage().toString() + "," + position.toString());

		addBehaviour(new ContractNetInitiator(this, msg) {
			private static final long serialVersionUID = 1L;

			protected void handlePropose(ACLMessage propose, Vector v) {
				System.out.println(propose.getSender().getLocalName()
						+ "@" + getLocalName() +": I'm a " + type.nextStage().toString() + " machine " + propose.getContent() + "km from you (answering CFP with a propose)");
			}

			protected void handleRefuse(ACLMessage refuse) {
//				System.out.println("Agent " + refuse.getSender().getName()
//						+ " refused");
			}

			protected void handleFailure(ACLMessage failure) {
				if (failure.getSender().equals(myAgent.getAMS())) {
					// FAILURE notification from the JADE runtime: the receiver
					// does not exist
					System.out.println("Responder does not exist");
				} else {
					System.out.println("Agent " + failure.getSender().getName()
							+ " failed");
				}
				// Immediate failure --> we will not receive a response from
				// this agent
			}

			protected void handleAllResponses(Vector responses,
					Vector acceptances) {
				// if (responses.size() < nResponders) {
				// Some responder didn't reply within the specified timeout
				// System.out.println("Timeout expired: missing "+(nResponders -
				// responses.size())+" responses");
				// }
				// Evaluate proposals.
				float bestProposal = 1000000f;
				AID bestProposer = null;
				ACLMessage accept = null;
				Enumeration e = responses.elements();
				while (e.hasMoreElements()) {
					ACLMessage msg = (ACLMessage) e.nextElement();
					if (msg.getPerformative() == ACLMessage.PROPOSE) {
						ACLMessage reply = msg.createReply();
						reply.setPerformative(ACLMessage.REJECT_PROPOSAL);
						acceptances.addElement(reply);
						float proposal = Float.parseFloat(msg.getContent());
						// finding the lowest cost proposal/plan
						if (proposal > 0 && proposal < bestProposal) {
							bestProposal = proposal;
							bestProposer = msg.getSender();
							accept = reply;
						}
					}
				}
				// Accept the proposal of the best proposer
				if (accept != null) {
					System.out.println( getLocalName() + ": I choose " + bestProposer.getLocalName() + " as my next " + type.nextStage().toString() + " (proposal accepted)");
					accept.setPerformative(ACLMessage.ACCEPT_PROPOSAL);
				}
			}

			protected void handleInform(ACLMessage inform) {
//				System.out.println("Agent " + inform.getSender().getName()
//						+ " successfully performed the requested action");
				nextMachine = inform.getSender();
				nextMachinePosition = new Position(inform.getContent());
				if(currentProduct != null){
					negotiateWithAllAgvs();
				}
//				System.out.println(getLocalName() + "'s nextMachine is " + nextMachine.getLocalName());
			}
		});

	}

	private void AnswerOtherMachines() {
		MessageTemplate template = MessageTemplate
				.and(MessageTemplate
						.MatchProtocol(FIPANames.InteractionProtocol.FIPA_CONTRACT_NET),
						MessageTemplate.MatchPerformative(ACLMessage.CFP));

		addBehaviour(new ContractNetResponder(this, template) {
			protected ACLMessage handleCfp(ACLMessage cfp)
					throws NotUnderstoodException, RefuseException {
//				System.out.println("Agent " + getLocalName()
//						+ ": CFP received from " + cfp.getSender().getName()
//						+ ". Action is " + cfp.getContent());

				String[] msgs = cfp.getContent().split(",");

				float proposal = evaluateAction(msgs[0], msgs[1]);
				if (proposal > 2) {
					// We provide a proposal
//					System.out.println("Agent " + getLocalName()
//							+ ": Proposing " + proposal);
					ACLMessage propose = cfp.createReply();
					propose.setPerformative(ACLMessage.PROPOSE);
					propose.setContent(String.valueOf(proposal));
					return propose;
				} else {
					// We refuse to provide a proposal
//					System.out.println("Agent " + getLocalName() + ": Refuse");
					throw new RefuseException("evaluation-failed");
				}
			}

			private float evaluateAction(String stage, String pos) {
				if (type.toString().equals(stage) && currentProduct == null && !waiting) {
					Position p = new Position(pos);
					return p.findDistance(position);
				}
				return -1f;
			}

			protected ACLMessage handleAcceptProposal(ACLMessage cfp,
					ACLMessage propose, ACLMessage accept)
					throws FailureException {
//				System.out.println("Agent " + getLocalName()
//						+ ": Proposal accepted");
				if (performAction()) {
//					System.out.println("Agent " + getLocalName()
//							+ ": Action successfully performed");
					ACLMessage inform = accept.createReply();
					inform.setContent(position.toString());
					inform.setPerformative(ACLMessage.INFORM);
					return inform;
				} else {
					System.out.println("Agent " + getLocalName()
							+ ": Action execution failed");
					throw new FailureException("unexpected-error");
				}
			}

			private boolean performAction() {
				return true;
			}

			protected void handleRejectProposal(ACLMessage cfp,
					ACLMessage propose, ACLMessage reject) {
//				System.out.println("Agent " + getLocalName()
//						+ ": Proposal rejected");
			}
		});
	}

	private void answerAgvsRequests() {
		addBehaviour(new CyclicBehaviour(this) {
			private static final long serialVersionUID = 1L;

			@Override
			public void action() {
				ACLMessage msg = receive(MessageTemplate
						.MatchPerformative(ACLMessage.REQUEST));
				if (msg != null) {
					if (msg.getContent().equals("pickup") && currentProduct != null) {
						pickupHandler(msg.getSender());
					}else{
						try {
							if (msg.getContentObject() instanceof Product) {
								dropoffHandler((Product)msg.getContentObject(), msg.getSender());
							}
						} catch (Exception e) {
						}
					}

				}
			}
		});

	}
	
	private void dropoffHandler(Product p, AID aid){
		currentProduct = p;
		myGui.setProductName(p.getName());
		System.out.println(getLocalName() + "@" + aid.getLocalName() + ": I'm receiving a " + p.getName());
		updateStatusOnFactory();
		startProcessing();
	}

	private void pickupHandler(AID aid) {
		System.out.print(getLocalName() + "@" + aid.getLocalName());
		System.out.println(": I'm giving you a " + currentProduct.getName());
		currentProduct = null;
		myGui.setProductName("no current Product");
		waiting= false;
		updateStatusOnFactory();
	}

	private void updateStatusOnFactory() {

		ACLMessage msg = new ACLMessage(ACLMessage.INFORM);
		try {
			msg.setContentObject(new MachineDetails(this));
		} catch (IOException e) {
			e.printStackTrace();
		}

		msg.addReceiver(factory);
		send(msg);
	}

	private void getFactory() {
		try {
			DFAgentDescription template = new DFAgentDescription();
			ServiceDescription templateSd = new ServiceDescription();
			templateSd.setType("factory");
			template.addServices(templateSd);

			DFAgentDescription[] results = DFService.search(this, template);
			if (results.length > 0) {
				System.out.println("Agent " + getLocalName()
						+ " found the following factory services:");
				for (int i = 0; i < results.length; ++i) {
					DFAgentDescription dfd = results[i];
					AID provider = dfd.getName();
					factory = provider;
					// agvs.add(provider);
					System.out.println("\t - " + provider.getLocalName());

					// //The same agent may provide several services; we are
					// only interested
					// Iterator it = dfd.getAllServices();
					// while (it.hasNext()) {
					// ServiceDescription sd = (ServiceDescription) it.next();
					// if (sd.getType().equals("weather-forecast")) {
					// System.out.println("- Service \""+sd.getName()+"\" provided by agent "+provider.getName());
					// }
					// }
				}
			} else {
				System.out.println("Agent " + getLocalName()
						+ " did not find any factory service");
			}
		} catch (FIPAException fe) {
			fe.printStackTrace();
		}
	}

	private void getAgvList() {
		try {
			DFAgentDescription template = new DFAgentDescription();
			ServiceDescription templateSd = new ServiceDescription();
			templateSd.setType("agv");
			template.addServices(templateSd);

			DFAgentDescription[] results = DFService.search(this, template);
			if (results.length > 0) {
				System.out.println("Agent " + getLocalName()
						+ " found the following agv services:");
				for (int i = 0; i < results.length; ++i) {
					DFAgentDescription dfd = results[i];
					AID provider = dfd.getName();
					agvs.add(provider);
					System.out.println("\t - " + provider.getLocalName());
				}
			} else {
				System.out.println("Agent " + getLocalName()
						+ " did not find any agv service");
			}
		} catch (FIPAException fe) {
			fe.printStackTrace();
		}
	}

	private void getMacList() {
		try {
			DFAgentDescription template = new DFAgentDescription();
			ServiceDescription templateSd = new ServiceDescription();
			templateSd.setType("machine");
			template.addServices(templateSd);

			DFAgentDescription[] results = DFService.search(this, template);
			if (results.length > 0) {
				System.out.println("Agent " + getLocalName()
						+ " found the following agv services:");
				for (int i = 0; i < results.length; ++i) {
					DFAgentDescription dfd = results[i];
					AID provider = dfd.getName();
					macs.add(provider);
					System.out.println("\t - " + provider.getLocalName());

					// //The same agent may provide several services; we are
					// only interested
					// Iterator it = dfd.getAllServices();
					// while (it.hasNext()) {
					// ServiceDescription sd = (ServiceDescription) it.next();
					// if (sd.getType().equals("weather-forecast")) {
					// System.out.println("- Service \""+sd.getName()+"\" provided by agent "+provider.getName());
					// }
					// }
				}
			} else {
				System.out.println("Agent " + getLocalName()
						+ " did not find any mac service");
			}
		} catch (FIPAException fe) {
			fe.printStackTrace();
		}
	}

	private void registerOnDF() {
		// registar no DF
		DFAgentDescription dfd = new DFAgentDescription();

		ServiceDescription sd1 = new ServiceDescription();
		sd1.setName(getName());
		sd1.setType("machine");

		ServiceDescription sd2 = new ServiceDescription();
		sd2.setType(type.toString());
		sd2.setName(getName());

		dfd.setName(getAID());
		dfd.addServices(sd1);
		dfd.addServices(sd2);

		try {
			DFService.register(this, dfd);
		} catch (FIPAException e) {
			System.out.println(getLocalName() + "Cannot register with DF");
			doDelete();
		}

	}

	private void initGui() {
		myGui = new MachineGui(this);
		myGui.showGui();
	}

	public MachineAgent(Stage type, Position position) {
		this.type = type;
		this.position = position;
	}

	@Override
	protected void onGuiEvent(GuiEvent arg0) {
		System.out.println("Some GUI Event");
		System.out.println(this.toString());
	}

	public Position getPosition() {
		return position;
	}

	public Product getCurrentProduct() {
		return currentProduct;
	}

	public boolean hasProduct() {
		return isProcessing();
	}

	public boolean isProcessing() {
		return (currentProduct != null) ? true : false;
	}

	public String toString() {
		String s = "-- Machine Agent --\n";
		s += "name: " + getLocalName() + "\n";
		s += "position: " + position + "\n";
		s += "type: " + type + "\n";
		if (isProcessing()) {
			s += "processing: yes\n";
		} else {
			s += "processing: no\n";
		}
		s += "---------------------";

		return s;
	}

	public Stage getType() {
		return type;
	}

	public void startNewProduct(Product p) {
		currentProduct = p;
		startProcessing();
	}

	private void startProcessing() {

		final TickerBehaviour processing = new TickerBehaviour(this, 100) {
			protected void onTick() {
				processingFinished = false;
				try{
				System.out.println(myAgent.getLocalName() + ": I'm "
						+ type.processToString() + " "
						+ currentProduct.getName() + " - " + getTickCount()
						* operationDuration / 100 + "%");
				myGui.setProductName(currentProduct.getName() + "("
						+ getTickCount() * operationDuration / 100 + "%)");
				currentProduct.setPercentage( getTickCount() * operationDuration / 100 + 10);
				updateStatusOnFactory();
				}catch(Exception e)
				{
					
				}
			}
		};
		addBehaviour(processing);

		WakerBehaviour end = new WakerBehaviour(this, operationDuration) {
			protected void handleElapsedTimeout() {
				processingFinished = true;
				try{
				System.out.println(myAgent.getLocalName() + ": I'm "
						+ type.processToString() + " a "
						+ currentProduct.getName() + " - " + "100%");
				myGui.setProductName(currentProduct.getName() + "(100%)");
				System.out.println(myAgent.getLocalName() + ": I've finished "
						+ type.processToString() + " a "
						+ currentProduct.getName());
				myGui.setProductName(currentProduct.getName());
				currentProduct.setPercentage(100);
				}catch(Exception e)
				{
					
				}
				if (type.nextStage().equals(Stage.dispatch)) {
					System.out.println(myAgent.getLocalName()
							+ ": "	+ currentProduct.getName() + " dispatched overseas!");
					currentProduct = null;
					waiting = false;
					updateStatusOnFactory();
				} else {
					if(currentProduct != null){
					negotiateWithAllAgvs();
					}
				}
				removeBehaviour(processing);
			}
		};
		addBehaviour(end);

	}

	public boolean isWaiting() {
		return waiting;
	}

}
